作者:陈广
日期:2018-4-15
学了点 JavaScript 和 TypeScript,终于可以开工我的目录树了。对于程序的世界,还有什么比直接做东西更快的学习方法吗?暂时我还没想到。虽然做的过程中可能由于掌握的知识不够而导致各种困难和痛苦,但完成后所带给你的收获,以及对知识点的理解深度,是看书和视频是完全无法比拟的。
首先还是要列出需要完成什么样的功能,然后根据需求决定采用什么数据结构,然后代码实现。
简而言之,我做的东西很简单。只要服务器给个目录列表,我在浏览器用 JavaScript 展示出来就可以了。当然子结点可展开折叠这是要有的。
之前见过两种目录树表示的两种数据结构:
当然用什么样的数据结构是根据需求来定的,这样用,肯定有这样用的道理。我的需求如此简单,自然就想到用一种更简单、更容易操作的数据结构来实现了。那就来创造一个吧:
就这么简单,来个例子演示一下:
以上层次结构就表示为:
条目 | 层次 |
---|---|
A | 0 |
B | 0 |
C | 1 |
D | 2 |
E | 2 |
F | 1 |
G | 0 |
这样表示是有前提的,因为使用数组索引作为其 ID,在插入一个新节点时,其后结点索引都会改变。只是没有增加、删除操作,所以可以这样设计。另外还需注意,一个层次为0的结点如果插入一个层次为2的结点紧随其后,这显然是不对的,这样的错误显然很容易发生,但可以在程序中判断和控制。当然,我们不需要插入功能。
将表示条目的数组转化为 HTML 注入到窗体大概是目录树中最难的算法了,一般是需要使用递归来完成的。但我惊喜地发现,不需要递归也能完成。而且只需一个循环,算法复杂度为O(n)。看来这个数据结构的设计是相当好啊!
实现还是经历了一翻周折,甚至 JavaScript 有些地方让我大跌眼镜。实现得还有些不尽如人意的地方,没办法,无人可问,以后慢慢解决吧。目录树使用 TypeScript 实现,页面直接用 JavaScript。
let selectNode: any; //当前选中结点
class CgTree {
nodes: CgNode[];
//构造函数
constructor(div: any, nodes: CgNode[]) {
selectNode = null;
this.nodes = nodes;
div.innerHTML = this.GetHtmlStr(); //注入 HTML
let liList = div.getElementsByClassName("cgTree_entryLink");
for (let i = 0; i < liList.length; i++) { //加入事件
liList[i].addEventListener("click", this.Click);
}
}
//计算结点 HTML
private GetHtmlStr(): string {
let str: string = `<ul>`;
let space: string = "";
for (let i = 0; i < this.nodes.length; i++) {
let current = this.nodes[i].level; //当前结点等级
let next = (i == this.nodes.length - 1) ? 0 : this.nodes[i + 1].level; //下一结点等级
space = "";
let temp = current;
while (temp--) {
space += "      ";
}
if (current < next) { //有孩子
str += `<li><a id="${i}" class="cgTree_entryLink">${space + this.nodes[i].name}</a><ul>`
}
else {
str += `<li><a id="${i}" class="cgTree_entryLink">${space + this.nodes[i].name}</a></li>`
if (current > next) {
let sub = current - next;
while (sub--) {
str += `</ul></li>`;
}
}
}
}
str += `</ul>`;
return str;
}
//结点单击事件方法
Click(e): void {
if (selectNode != undefined) { //选中结点换CSS
selectNode.setAttribute("class", "cgTree_entryLink");
}
this.setAttribute("class", "cgTree_entryLinkSelected");
selectNode = this;
//防止事件冒泡
e.stopPropagation();
}
}
class CgNode {
//私有成员变量,用于属性
private _name: string; //条目名称,用于显示
private _url: string = "#"; //单击转到的链接地址
private _level: number; //第几层条目,最顶层为0
private _isOpen: boolean = false; //是否展开
//属性
get name(): string {
return this._name;
}
get url(): string {
return this._url;
}
get level(): number {
return this._level;
}
get isOpen(): boolean {
return this._isOpen;
}
set isOpen(open: boolean) {
this._isOpen = open;
}
constructor(name: string, url: string, lvl: number) {
this._name = name;
this._url = url;
this._level = lvl;
}
}
由于事件方法包装在类里面,无法直接在 HTML 中关联,只好使用代码加了。我也不知道是否可以在 HTML 中关联类里的方法。另外还声明了一个全局变量selectNode
,本应该声明为CgTree
的成员变量,但死活记不住状态,搞得我很无语,只能用全局变量了,这破坏了面向对象的原则。JavaScript 的内部机理我是一窍不通啊。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="stylesheet" href="demo.css">
</head>
<body>
<div id="Container"></div>
<p>请输入新条目内容:<input type="text" name="aaa" id="nodeName"></p>
<button onclick="AddNode(1)">作为子目录加入</button> 
<button onclick="AddNode(0)">作为同级目录加入</button>
</body>
<script src="demo.js"></script>
<script src="CgTree.js"></script>
</html>
没啥好说的,够简单。加入了添加新结点功能,只是为了测试。
var cgTree;
var cgNodes
var container
window.onload = function () {
container = document.getElementById("Container"); //获取id为Container的div
cgNodes = [
new CgNode("A", "#", 0),
new CgNode("B", "#", 0),
new CgNode("C", "#", 1),
new CgNode("D", "#", 1),
];
cgTree = new CgTree(container, cgNodes); //创建bitEdit并传入div,在此div内画出按钮
};
//在当前元素后插入新结点,isChild表示是否作为孩子插入
function AddNode(isChild) {
if(selectNode==null)
{
alert("请先选择一个目录作为插入点!");
return;
}
var index = selectNode.getAttribute("id");
var selNodeLevel = cgNodes[index].level;
var level = isChild ? selNodeLevel + 1 : selNodeLevel;
var newNode = new CgNode(document.getElementById("nodeName").value, "#", level);
cgNodes.splice(++index, 0, newNode);
cgTree = new CgTree(container, cgNodes);
}
添加新结点功能只是为了测试画的是否正确。CgTree 本身是没有这个功能的,所以每添加一个结点都会创建新的 CgTree 以重画。
body {
background-color: #ddd;
}
#Container {
width: 400px;
border: 4px solid darkgoldenrod;
border-radius: 3px;
background-color: coral;
}
#Container ul {
list-style: none;
padding: 0;
margin: 0;
}
#Container li {
list-style: none;
margin: 0;
}
.cgTree_entryLink {
display: block;
cursor: pointer;
color: #4e4e4e;
background: #eeeeee;
-moz-box-shadow: 0 1px 0 white inset, 0 -1px 0 #d4d4d4 inset;
-webkit-box-shadow: 0 1px 0 white inset, 0 -1px 0 #d4d4d4 inset;
box-shadow: 0 1px 0 white inset, 0 -1px 0 #d4d4d4 inset;
/* text-shadow: 0 -1px 0 rgba(255, 255, 255, 0.3); */
padding: 4px 4px;
}
.cgTree_entryLinkSelected {
display: block;
cursor: pointer;
color: #6e6e6e;
background: #CFBB87;
-moz-box-shadow: 0 1px 0 white inset, 0 -1px 0 #d4d4d4 inset;
-webkit-box-shadow: 0 1px 0 white inset, 0 -1px 0 #d4d4d4 inset;
box-shadow: 0 1px 0 white inset, 0 -1px 0 #d4d4d4 inset;
text-shadow: 0 -1px 0 rgba(255, 255, 255, 0.3);
padding: 4px 4px;
}
.cgTree_entryLink:hover {
background: #dfdfdf;
}
这个没啥好讲的,网上拷贝大法。哪个好看拷哪个。
放到窗体上给大家自己玩。